import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged, signOut, signInWithEmailAndPassword } from 'firebase/auth'; import { getFirestore, doc, setDoc, getDoc, collection, onSnapshot, addDoc, updateDoc, deleteDoc } from 'firebase/firestore'; import { Play, Pause, Music, Calendar, Image as ImageIcon, Mail, Lock, LogOut, Plus, Trash2, Edit, Check, ExternalLink, Volume2, Instagram, Youtube, Send, X } from 'lucide-react'; // Инициализация Firebase. // При развертывании на Firebase Hosting эти переменные будут автоматически заменяться вашими реальными данными. const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : { apiKey: "", authDomain: "artist-portfolio-demo.firebaseapp.com", projectId: "artist-portfolio-demo", storageBucket: "artist-portfolio-demo.appspot.com", messagingSenderId: "123456789", appId: "1:123456789:web:abcdef" }; const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'artist-portfolio'; export default function App() { // Состояние авторизации const [user, setUser] = useState(null); const [isAdmin, setIsAdmin] = useState(false); const [showLoginModal, setShowLoginModal] = useState(false); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [loginError, setLoginError] = useState(''); // Активная вкладка на сайте const [activeTab, setActiveTab] = useState('home'); // Динамические данные из Firestore const [tracks, setTracks] = useState([]); const [concerts, setConcerts] = useState([]); const [photos, setPhotos] = useState([]); const [bio, setBio] = useState({ name: 'NEON ECHO', genre: 'Synthwave / Indie Electronic', about: 'Музыкальный проект, вдохновленный ночным мегаполисом, неоновыми огнями 80-х и глубоким современным басом. Каждое выступление — это аудиовизуальное погружение в атмосферу бесконечной ночи.', avatar: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?auto=format&fit=crop&w=800&q=80', heroBg: 'https://images.unsplash.com/photo-1514525253161-7a46d19cd819?auto=format&fit=crop&w=1920&q=80' }); // Состояние воспроизведения музыки const [currentTrack, setCurrentTrack] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const audioRef = useRef(null); // Формы редактирования (Admin Mode) const [isEditingBio, setIsEditingBio] = useState(false); const [editedBio, setEditedBio] = useState({ ...bio }); const [newTrack, setNewTrack] = useState({ title: '', album: '', duration: '', audioUrl: '', coverUrl: '', spotify: '', yandex: '' }); const [newConcert, setNewConcert] = useState({ date: '', city: '', venue: '', ticketUrl: '', isSoldOut: false }); const [newPhoto, setNewPhoto] = useState({ url: '', description: '' }); // Лайтбокс для просмотра фото const [selectedPhoto, setSelectedPhoto] = useState(null); // Обратная связь const [contactForm, setContactForm] = useState({ name: '', email: '', message: '' }); const [contactSent, setContactSent] = useState(false); // 1. Аутентификация (Правило №3) useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Ошибка аутентификации:", error); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (currentUser) => { setUser(currentUser); // Если вошел анонимно — он гость. Если под email/password (админ) — даем права if (currentUser && !currentUser.isAnonymous) { setIsAdmin(true); } else { setIsAdmin(false); } }); return () => unsubscribe(); }, []); // 2. Получение данных из Firestore в реальном времени (Правило №1 и №2) useEffect(() => { if (!user) return; // Защита от запросов без авторизации // Публичные коллекции по строгому пути /artifacts/{appId}/public/data/{collection} const tracksCollection = collection(db, 'artifacts', appId, 'public', 'data', 'tracks'); const concertsCollection = collection(db, 'artifacts', appId, 'public', 'data', 'concerts'); const photosCollection = collection(db, 'artifacts', appId, 'public', 'data', 'photos'); const bioDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'bio', 'main'); // Слушаем треки const unsubTracks = onSnapshot(tracksCollection, (snapshot) => { const list = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setTracks(list); }, (error) => console.error("Ошибка получения треков:", error)); // Слушаем концерты const unsubConcerts = onSnapshot(concertsCollection, (snapshot) => { const list = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); // Сортируем даты в памяти, избегая сложных запросов Firestore (Правило №2) list.sort((a, b) => new Date(a.date) - new Date(b.date)); setConcerts(list); }, (error) => console.error("Ошибка получения концертов:", error)); // Слушаем галерею const unsubPhotos = onSnapshot(photosCollection, (snapshot) => { const list = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); setPhotos(list); }, (error) => console.error("Ошибка получения фото:", error)); // Слушаем био/информацию об артисте const unsubBio = onSnapshot(bioDocRef, (docSnap) => { if (docSnap.exists()) { const data = docSnap.data(); setBio(data); setEditedBio(data); } else { // Если документа еще нет, создаем дефолтный setDoc(bioDocRef, bio); } }, (error) => console.error("Ошибка получения био:", error)); return () => { unsubTracks(); unsubConcerts(); unsubPhotos(); unsubBio(); }; }, [user]); // Воспроизведение аудио локально const handlePlayPause = (track) => { if (currentTrack?.id === track.id) { if (isPlaying) { audioRef.current.pause(); setIsPlaying(false); } else { audioRef.current.play().catch(e => console.log("Ошибка воспроизведения:", e)); setIsPlaying(true); } } else { setCurrentTrack(track); setIsPlaying(true); // Устанавливаем аудиофайл (если URL нет, используем демонстрационный беззвучный или фоновый трек) const src = track.audioUrl || "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"; if (audioRef.current) { audioRef.current.src = src; audioRef.current.play().catch(e => console.log("Ошибка воспроизведения:", e)); } } }; // Авторизация администратора (обычный вход по почте/паролю) const handleLogin = async (e) => { e.preventDefault(); setLoginError(''); try { await signInWithEmailAndPassword(auth, email, password); setShowLoginModal(false); setEmail(''); setPassword(''); } catch (error) { setLoginError('Неверный логин или пароль. Обратите внимание, что для создания админа вам нужно сначала зарегистрировать его в Firebase Console.'); } }; // Выход из админки (возврат в анонимный режим чтения) const handleLogout = async () => { try { await signOut(auth); await signInAnonymously(auth); } catch (error) { console.error("Ошибка выхода:", error); } }; // --- Функции администратора для работы с БД (Firestore) --- // Сохранить био const saveBio = async () => { const bioDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'bio', 'main'); await setDoc(bioDocRef, editedBio); setIsEditingBio(false); }; // Добавить трек const addTrack = async (e) => { e.preventDefault(); if (!newTrack.title) return; const tracksCollection = collection(db, 'artifacts', appId, 'public', 'data', 'tracks'); await addDoc(tracksCollection, { ...newTrack, audioUrl: newTrack.audioUrl || "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3", coverUrl: newTrack.coverUrl || "https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?auto=format&fit=crop&w=400&q=80" }); setNewTrack({ title: '', album: '', duration: '', audioUrl: '', coverUrl: '', spotify: '', yandex: '' }); }; // Удалить трек const deleteTrack = async (id) => { const trackDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'tracks', id); await deleteDoc(trackDocRef); if (currentTrack?.id === id) { audioRef.current.pause(); setIsPlaying(false); setCurrentTrack(null); } }; // Добавить концерт const addConcert = async (e) => { e.preventDefault(); if (!newConcert.city || !newConcert.date) return; const concertsCollection = collection(db, 'artifacts', appId, 'public', 'data', 'concerts'); await addDoc(concertsCollection, newConcert); setNewConcert({ date: '', city: '', venue: '', ticketUrl: '', isSoldOut: false }); }; // Удалить концерт const deleteConcert = async (id) => { const concertDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'concerts', id); await deleteDoc(concertDocRef); }; // Добавить фото const addPhoto = async (e) => { e.preventDefault(); if (!newPhoto.url) return; const photosCollection = collection(db, 'artifacts', appId, 'public', 'data', 'photos'); await addDoc(photosCollection, newPhoto); setNewPhoto({ url: '', description: '' }); }; // Удалить фото const deletePhoto = async (id) => { const photoDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'photos', id); await deleteDoc(photoDocRef); }; // Имитация отправки формы обратной связи const handleContactSubmit = (e) => { e.preventDefault(); if (!contactForm.email || !contactForm.message) return; setContactSent(true); setTimeout(() => { setContactSent(false); setContactForm({ name: '', email: '', message: '' }); }, 4000); }; // Вспомогательные данные по умолчанию (если БД пуста) const defaultTracks = [ { id: '1', title: 'Neon Horizon', album: 'Retro City', duration: '3:45', coverUrl: 'https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?auto=format&fit=crop&w=400&q=80', audioUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' }, { id: '2', title: 'Midnight Drive', album: 'Nightfall', duration: '4:12', coverUrl: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?auto=format&fit=crop&w=400&q=80', audioUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3' }, { id: '3', title: 'Cyber Pulse', album: 'Future Retro', duration: '3:58', coverUrl: 'https://images.unsplash.com/photo-1514525253161-7a46d19cd819?auto=format&fit=crop&w=400&q=80', audioUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3' } ]; const defaultConcerts = [ { id: '1', date: '2026-07-15', city: 'Москва', venue: 'Клуб "Урбан"', ticketUrl: '#', isSoldOut: false }, { id: '2', date: '2026-07-22', city: 'Санкт-Петербург', venue: 'Космонавт', ticketUrl: '#', isSoldOut: false }, { id: '3', date: '2026-08-05', city: 'Сочи', venue: 'Зеленый Театр', ticketUrl: '#', isSoldOut: true } ]; const defaultPhotos = [ { id: '1', url: 'https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?auto=format&fit=crop&w=800&q=80', description: 'Live Сет в Москве' }, { id: '2', url: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?auto=format&fit=crop&w=800&q=80', description: 'Запись альбома, 2026' }, { id: '3', url: 'https://images.unsplash.com/photo-1501386761578-eac5c94b800a?auto=format&fit=crop&w=800&q=80', description: 'Фестиваль На открытом воздухе' }, { id: '4', url: 'https://images.unsplash.com/photo-1506157786151-b8491531f063?auto=format&fit=crop&w=800&q=80', description: 'Промо фотосессия' } ]; const displayTracks = tracks.length > 0 ? tracks : defaultTracks; const displayConcerts = concerts.length > 0 ? concerts : defaultConcerts; const displayPhotos = photos.length > 0 ? photos : defaultPhotos; return (